-- ============================================================================
-- ModbusRTU_Master.lua
-- author:          read
-- version          1.0.0
-- Created on:      2025-06-16
-- Last modified:   2025-07-04
-- 描述:
--   本库实现了完整的 Modbus RTU 主站功能，支持多种功能码操作，
--   提供简洁易用的 API 接口，适用于工业自动化场景。
-- ============================================================================

local ModbusRTU_Master = {}
ModbusRTU_Master.__index = ModbusRTU_Master

-- 默认配置
local DEFAULT_CONFIG = {
    uartid = 1, -- 串口ID
    baudrate = 9600, -- 波特率
    databits = 8, -- 数据位
    stopbits = 1, -- 停止位
    parity = uart.None, -- 校验位
    endianness = uart.LSB, -- 字节序
    buffer_size = 1024, -- 缓冲区大小
    -- gpio_485 = 25, -- 485转向GPIO
    -- rx_level = 0, -- 485模式下RX的GPIO电平
    -- tx_delay = 10000 -- 485模式下TX向RX转换的延迟时间（us）
}

-- === 添加 new() 构造函数 ===
function ModbusRTU_Master:new(config)
    local self = setmetatable({}, ModbusRTU_Master)
    self:init(config or {})
    return self
end

-- === 初始化主站 ===
function ModbusRTU_Master:init(config)
    config = config or {}
    -- 遍历 DEFAULT_CONFIG，为缺少的参数赋值
    for key, default_value in pairs(DEFAULT_CONFIG) do
        if config[key] == nil then
            config[key] = default_value
        end
    end

    self.config = config --  保存配置

    -- 初始化UART
    uart.setup(
    config.uartid,
    config.baudrate,
    config.databits,
    config.stopbits,
    config.parity,
    config.endianness,
    config.buffer_size,
    config.gpio_485,
    config.rx_level,
    config.tx_delay
)
    log.info("ModbusRTU_Master 当前串口初始化配置为:", json.encode(config))
end

--- CRC16 计算
-- @param data 待计算CRC的数据
-- @return number 返回计算出的CRC值
function ModbusRTU_Master:crc16(data)
    local crc = 0xFFFF
    for i = 1, #data do
        crc = crc ~ data:byte(i)
        for _ = 1, 8 do
            local j = crc & 1
            crc = crc >> 1
            if j ~= 0 then
                crc = crc ~ 0xA001  -- 0xA001 是 CRC16 的生成多项式
            end
        end
    end
    return crc
end

-- 解析从站返回的Modbus帧
-- @param data string 从站返回的原始数据
-- @return table|nil, string 成功返回解析后的表，失败返回nil和错误信息
function ModbusRTU_Master:parse_frame(data)
    -- 检查帧长度是否足够(最小帧长度 = 地址1 + 功能码1 + 数据长度1 + CRC2)
    if not data or #data < 5 then  -- 最小帧长度检查
    log.info("Modbus帧解析失败", "帧长度不足")
        return nil, "帧长度不足"
    end

    local addr = data:byte(1) or ""                  -- 设备地址（从站地址）
    local fun = data:byte(2) or ""                   -- 功能码
    local byte_count = data:byte(3) or ""              -- 数据域长度

    -- 处理异常帧 异常帧长度固定为5字节 [地址] [异常功能码] [错误码] [CRC低字节] [CRC高字节]
    if fun & 0x80 ~= 0 then
        if #data ~= 5 then
            log.warn("Modbus异常帧不完整")
            return nil, "异常帧不完整"
        end
        local exception_code = data:byte(3) -- 异常码
        local crc_received = data:byte(4)+data:byte(5)*256      -- 解包CRC值(大端格式)
        local crc_calculated = self:crc16(data:sub(1, -3))
        if crc_received == crc_calculated then
            log.info("Modbus异常帧CRC校验成功")
            return {
                type = "exception",
                addr = addr,
                fun = fun,
                exception_code = exception_code,
                crc = crc_received
            }
        else
            log.warn("Modbus异常帧CRC校验失败")
            return nil, "CRC校验失败"
        end
    end


    -- 根据功能码处理不同的帧结构
    local payload,payload_start,payload_len

    -- 通用功能码处理
    if fun == 0x01 or fun == 0x02 or fun == 0x03 or fun == 0x04 then
        -- 读线圈/离散输入/保持寄存器/输入寄存器（需要数据长度字段）
        if #data < (3 + byte_count + 2) then
            log.error("Modbus帧解析失败","读操作响应不完整")
            return nil,"数据域不完整"
        end
        payload_start = 4
        payload_len = byte_count
    elseif fun == 0x05 or fun == 0x06 or fun == 0x0F or fun == 0x10 then
        -- 写单线圈/单寄存器/多线圈/多寄存区（无需数据长度字段）
        if fun == 0x05 or fun == 0x06 then
            --写单个线圈/寄存器（响应包含地址和值）
            if #data < 8 then
                log.error("Modbus帧解析失败","写单操作响应不完整")
                return nil,"数据域不完整"
            end
            payload_start = 3
            payload_len = 4 -- 写单操作响应包含地址和值，所以数据长度为4字节
        elseif fun == 0x0F or fun == 0x10 then
            -- 写多线圈/寄存器(响应包含起始地址和数量)
            if #data < 8 then
                log.error("Modbus帧解析失败","写多操作响应不完整")
                return nil,"数据域不完整"
            end
            payload_start = 3
            payload_len = 4 -- 写多操作响应包含起始地址和数量，所以数据长度为4字节
        end
    else
        log.error("Modbus帧解析失败","未知功能码",string.format("0x%02x",fun)) 
        return nil,"未知功能码"
    end

    local payload = data:sub(payload_start, payload_start+payload_len - 1) or ""          -- 数据部分(从第4字节开始，长度由byte_count指定)
    local crc_data = data:sub(-2, -1) or ""                      -- 提取帧末尾的CRC值(最后2字节
    local idx, crc_received = pack.unpack(crc_data, "H")        -- 解包CRC值(大端格式)

    local crc_calculated = self:crc16(data:sub(1, -3))
    -- 校验CRC 计算CRC(排除最后2字节CRC)
    if  crc_received == crc_calculated then
        log.info("ModbusRTU_Master CRC校验成功")
    else
        log.warn("Modbus帧CRC校验失败","接收值=", crc_received,"计算值=", crc_calculated)
        return nil, "CRC 校验失败"
    end

    -- 构造返回结果
    local result = {
            addr = addr,            -- 设备地址
            fun = fun,              -- 功能码
            byte_count = byte_count,        -- 数据字节长度
            payload = payload,      -- 实际数据
            crc = crc_received    -- CRC校验值
        }
    
    -- 根据功能码解析数据域
    if fun == 0x01 or fun == 0x02 then -- 读线圈/离散输入
        result.type = "读线圈/离散输入"
        result.bit_count = byte_count * 8 -- 每个字节有8位，总位数为数据长度的8倍
        result.value = {}
        for i = 1,byte_count do
            local byte = data:byte(4 + i - 1)
            for j = 0,7 do
                table.insert(result.value, (byte & (1 << j)) ~= 0)
            end
        end
    elseif fun == 0x03 or fun == 0x04 then -- 读保持寄存器/输入寄存器响应帧结构：[地址][功能码][字节数][数据1][数据2][...][CRC低][CRC高]
        result.type = "保持寄存器/输入寄存器"
        result.reg_count = byte_count / 2 -- 每个寄存器有2个字节，总寄存器数为数据长度的一半
        result.value = {}
        for i = 1,byte_count,2 do
            local reg = data:byte(4 + i - 1) * 256 + data:byte(4 + i)
            table.insert(result.value, reg)
        end
    elseif fun == 0x05 then -- 写单个线圈响应帧结构：[地址][功能码][线圈地址高][线圈地址低][值高][值低][CRC低][CRC高]
        result.type = "写单个线圈"
        result.reg_addr = data:byte(3) * 256 + data:byte(4) -- 起始地址
        local raw_value = data:byte(5)*256 + data:byte(6) -- 寄存器值
        result.value = (raw_value == 0xFF00) and true or false
    elseif fun == 0x06 then -- 写寄存器响应帧结构：[地址][功能码][寄存器地址高][寄存器地址低][值高][值低][CRC低][CRC高]
        result.type = "写单个寄存器"
        result.reg_addr = data:byte(3) * 256 + data:byte(4) -- 起始地址
        result.value = data:byte(5) * 256 + data:byte(6) -- 寄存器值
    elseif fun == 0x0F or fun == 0x10 then -- 写多个线圈/寄存器响应帧结构：[地址][功能码][寄存器地址高][寄存器地址低][寄存器数量高][寄存器数量低][CRC低][CRC高]
        result.type = fun == 0x0F and "写多个线圈" or "写多个寄存器"
        result.reg_addr = data:byte(3) * 256 + data:byte(4) -- 起始地址
        result.reg_count = data:byte(5) * 256 + data:byte(6) -- 寄存器数量
    end
    log.info("Modbus帧解析成功","功能码=0x"..string.format("%02x",fun))
    return result
end

-- 构建Modbus请求帧
-- @param addr number 从站地址
-- @param fun number 功能码
-- @param data string 请求数据
-- @return string 完整的Modbus帧
function ModbusRTU_Master:build_frame(addr, fun, data)
    -- 构建帧头(地址 + 功能码 + 数据)
    local frame = string.char(addr, fun) .. data
    -- 计算CRC校验码
    local crc = self:crc16(frame)
    -- 解包CRC值(大端格式)
    local pack_crc = pack.pack("H", crc)
    -- 将CRC添加到帧尾(大端格式)
    return frame .. pack_crc
end

--- 发送Modbus命令
-- @param addr number 目标从站地址
-- @param fun number 功能码
-- @param data string 命令数据
-- @param interval number|nil 循环发送间隔(秒)，如果为nil则只发送一次
function ModbusRTU_Master:send_command(addr, fun, data, interval)
    local uartid = self.config.uartid  -- 从配置获取串口号

    -- 构建完整Modbus帧
    local cmd = self:build_frame(addr, fun, data)

    if interval then
        -- 循环发送模式
        sys.timerLoopStart(function()
            -- log.info("发送Modbus命令", "地址=", addr, "功能码=", fun, "长度=", #cmd, "数据=", cmd:toHex())
            uart.write(uartid, cmd)
        end, interval*1000)
        log.info("已启动循环发送", "间隔=", interval, "秒")
    else
        -- 只发送一次
        -- log.info("发送Modbus命令", "地址=", addr, "功能码=", fun, "长度=", #cmd, "数据=", cmd:toHex())
        -- log.info("只发送一次")
        uart.write(uartid, cmd)
    end
end

--- 设置数据接收回调函数
-- @param callback function 接收到数据时调用的函数
function ModbusRTU_Master:set_receive_callback(callback)

    local uartid = self.config.uartid

    -- 注册串口接收回调
    uart.on(uartid, "receive", function(id, len)

        -- 读取串口接收到的数据
        local data = uart.read(id, len or 0)
        if #data > 0 then
            log.info("收到Modbus响应", "长度=", #data)
            -- 解析Modbus帧
            local frame, err = self:parse_frame(data)
            if frame then
                -- 解析成功，调用回调函数
                callback(frame)
            else
                -- 解析失败记录错误
                log.warn("Modbus响应解析失败", err)
            end
        end

    end)
end

--- 设置数据发送完成回调
-- @param callback function 数据发送完成后调用的函数
function ModbusRTU_Master:set_sent_callback(callback)
    uart.on(self.config.uartid, "sent", function(id)
        log.info("ModbusRTU_Master数据发送完成", "串口号=", id)
        if callback then
            callback(id)
        end
    end)
end

---------------------------------------------------------------------------------
-------------------------  便捷API  ----------------------------------------------

--- 读取寄存器（功能码 0x03）
-- @param slave_addr number 从站地址 (1~247)
-- @param reg_addr number 起始寄存器地址 (0~65535)
-- @param reg_num number 读取寄存器数量 (1~125)
function ModbusRTU_Master:read_registers(slave_addr,reg_addr,reg_num)
    -- 参数校验
    if slave_addr < 1 or slave_addr > 247 then
        log.error("Modbus_Master","从站地址超出范围（1~247）")
        return
    end
    if reg_addr< 0 or reg_addr>0xFFFF then
        log.error("Modbus_Master","起始寄存器地址超出范围（0~65535）")
    end
    if reg_num< 1 or reg_num>125 then
        log.error("Modbus_Master","读取寄存器数量超出范围（1~125）")
    end

    local data = string.char(
        -- 起始寄存器 高位地址  -- 低位地址
        (reg_addr >> 8) & 0xFF, reg_addr & 0xFF,       
        -- 寄存器数量 高位地址 -- 低位数量
        (reg_num >> 8) & 0xFF,  reg_num & 0xFF          
    )

    self:send_command(slave_addr,0x03,data)
end


--- 写入寄存器（支持单个或多个寄存器）功能码0x06/0x10
-- @param slave_addr number 从站地址 (1~247)
-- @param reg_addr number 起始寄存器地址 (0~65535)
-- @param value number|table 写入的值（单个数值或寄存器值数组）
function ModbusRTU_Master:write_registers(slave_addr,reg_addr,value)
    -- 参数校验
    if slave_addr < 1 or slave_addr >247 then
        log.error("Modbus_Master","从站地址超出范围（1~247）")
        return
    end
    if reg_addr < 0 or reg_addr > 0xFFFF then
        log.error("Modbus_Master","起始寄存器地址超出范围（0~65535）")
        return
    end
    
    local fun,data,reg_num

    -- 自动判断功能码
    if type(value) == "number" then
        fun = 0x06 -- 单个寄存器写入
        reg_num = 1
        data = pack.pack(">HH",reg_addr,value)
    elseif type(value) == "table" then
        fun = 0x10 -- 多个寄存器写入
        reg_num = #value
        if reg_num < 1 or reg_num > 123 then
            log.error("Modbus_Master","寄存器数量超出范围（1~123）")
            return
        end

        -- 构建数据部分
        local byte_count = reg_num * 2
        local payload = pack.pack(">HHb",reg_addr,reg_num,byte_count)

        -- 添加寄存器（大端序）
        for i = 1,reg_num do
            local val = value[i] or 0 
            payload = payload..pack.pack(">H",val)
        end
        data = payload
    else
        log.error("Modbus_Master","写入值类型错误（number|table）")
        return
    end

    -- 发送命令
    self:send_command(slave_addr,fun,data)
    log.info("Modbus 发送写入命令", "地址=", slave_addr, "功能码=", fun, "长度=", #data, "数据=", data:toHex())
end

--- 读取线圈（功能码 0x01）
-- 报文：从站地址(1~247) 功能码(0x01) 起始地址(0~65535) 读取数量(1~2000) CRC校验    
-- @param slave_addr number 从站地址 (1~247)
-- @param reg_addr number 起始寄存器地址 (0~65535)
-- @param reg_num number 读取寄存器数量 (1~125)
function ModbusRTU_Master:read_coils(slave_addr,reg_addr,reg_num)
        -- 参数校验
        if slave_addr < 1 or slave_addr > 247 then
            log.error("ModbusRTU_Master:read_coils", "从站地址超出范围，请输入1~247")
            return
        end
        if reg_addr < 0 or reg_addr > 65535 then
            log.error("ModbusRTU_Master:read_coils", "寄存器地址超出范围，请输入0~65535")
            return
        end
        if reg_num < 1 or reg_num > 2000 then
            log.error("ModbusRTU_Master:read_coils", "线圈数量超出范围，请输入1~2000")
            return
        end

        local data = string.char((reg_addr >> 8) & 0xFF,reg_addr&0xFF,(reg_num>>8)&0xFF,reg_num & 0xFF)

        self:send_command(slave_addr,0x01,data)
end

--- 写入线圈（支持单个或多个线圈）功能码0x05/0x15
-- @param slave_addr number 从站地址 (1~247)
-- @param reg_addr number 起始寄存器地址 (0~65535)
-- @param value boolean|table 写入的值（单个线圈或多个线圈）
function ModbusRTU_Master:write_coils(slave_addr,reg_addr,value)
    -- 参数校验
    if slave_addr < 1 or slave_addr >247 then
        log.error("ModbusRTU_Master:write_coils","从站地址超出范围（1~247）")
        return
    end
    if reg_addr < 0 or reg_addr >  65535 then
        log.error("ModbusRTU_Master:write_coils","起始寄存器地址超出范围（0~65535）")
        return
    end
    
    local fun,data

    -- 自动判断是单个线圈还是多个线圈
    if type(value) == "boolean" or (type(value)=="number" and (value == 0x0000 or value == 0xFF00)) then
        fun = 0x05 -- 单个线圈写入
        local coil_value = (value == true or value == 0xFF00) and 0xFF00 or 0x0000
        data = string.char(
            (reg_addr >> 8) & 0xFF,
            reg_addr & 0xFF,
            (coil_value >> 8) & 0xFF,
            coil_value & 0xFF
        )
    elseif type(value) == "table" then
        fun = 0x0F -- 多个线圈写入
        local bit_count = #value
        if bit_count < 1 or bit_count > 1968 then
            log.error("ModbusRTU_Master:write_coils", "线圈数量超出范围，请输入1~1968")
            return
        end

        -- 构建 bit 字节流
        local byte_count = math.ceil(bit_count / 8)
        local bytes = {}

        for i=1,byte_count do 
            local b = 0
            for j=0,7 do
                local idx = (i-1)*8+j+1
                if idx <= bit_count and (value[idx] == true or value[idx] == 1 or value[idx] == 0xFF00) then
                    b = b + (1<<j)
                end
            end
            table.insert(bytes,b)
        end

        -- 构建数据字段
        data =string.char(
            (reg_addr >> 8)&0xFF, -- 高字节
            reg_addr&0xFF, -- 低字节
            (bit_count >> 8)&0xFF,
            bit_count&0xFF,
            byte_count
        )

        for _, v in ipairs(bytes) do
            data = data .. string.char(v)
        end

    else
        log.error("Modbus_Master","写入值类型错误（number|table）")
        return
    end

    -- 发送命令
    self:send_command(slave_addr,fun,data)
    log.info("Modbus 发送写入命令", "地址=", slave_addr, "功能码=", fun, "长度=", #data, "数据=", data:toHex())
end


return ModbusRTU_Master
